package tool.analyzer
 
trait TypeChecker {
	self: Reporter =>

	import tool.analyzer.Symbols._
	import tool.analyzer.InitialSymbols._
	import Types._
	import parser.Trees._

  	/** Typechecking does not produce a value, but has the side effect of
   	 * attaching types to trees and potentially outputting error messages. */
	def typeCheck(prog: Program, gs: GlobalScope): Unit = {
		tcMainObject(prog.main, gs)
		prog.classes.foreach(g => tcDecls(g))
	}

	def tcMainObject(main: MainObject, gs: GlobalScope): Unit = {
		tcStat(main.stat)
	}

	def tcDecls(t: Tree): Unit = t match{
		case ClassDecl(id, varDecls, metDecls) => metDecls.foreach(g => tcDecls(g))
		case ClassWithExtends(id, sup, varDecls, metDecls) => metDecls.foreach(g => tcDecls(g))
		case mt @ MethDecl(id, args, ret, throws, varDecls, sts, returnExpr) => 
			val retType = tcExpr(returnExpr)

			throws foreach { g =>
				if(!isThrowable(g.getSymbol.asInstanceOf[ExceptionSymbol].getType))
					error("Not an exception", g)
			}

			if(!retType.isSubTypeOf(mt.getSymbol.getType)) // if the return type doesn't exactly match : output error
				error("Expected " + mt.getSymbol.getType + " found " + retType, returnExpr)
			tcStat(sts)
		case _ =>
	}

    /** Suggested inner function:
     *
     * Computes the type of an expression. If exp is not empty, checks that
     * the expression is a subtype of one in exp. If it's not, prints an
     * error message and returns the first element of exp. Returning a valid
     * type despite the error is a way to do error recovering: type checking
     * will continue, assuming the correct type was found. */
	def tcExpr(expr: ExprTree, exp: Type*): Type = {

		val t:Type = expr match{
			//logical expressions
			case Not(e) => tcExpr(e, TBool)
			case And(e1, e2) => tcExpr(e1, TBool); tcExpr(e2, TBool)
			case Or(e1, e2) => tcExpr(e1, TBool); tcExpr(e2, TBool)

			//arithmetic expressions
			case Plus(e1, e2) =>
				val t1 = tcExpr(e1, TInt, TString)
				val t2 = tcExpr(e2, TInt, TString)
				if(t1 == TInt && t2 == TInt){
					TInt
				}else{
					TString
				}

			case Minus(e1, e2) => tcExpr(e1, TInt); tcExpr(e2, TInt)
			case Times(e1, e2) =>  tcExpr(e1, TInt); tcExpr(e2, TInt)
			case Div(e1, e2) => tcExpr(e1, TInt); tcExpr(e2, TInt)

		//tests 
			case LessThan(e1, e2) => tcExpr(e1, TInt); tcExpr(e2, TInt); TBool
			case eq @ Equals(e1, e2) => 
				val t1 = tcExpr(e1)
				val t2 = tcExpr(e2)
				t1 match{
					case TObject(_) =>
						t2 match{
							case TObject(_) => TBool
							case _ => error("Object types cannot be compared with primitive types", eq.pos); TBool
						}
					case typ @ _ => 
						if(t2 != typ){
							error("Two sides of == must have the same type", eq.pos); TBool
						}else
							TBool
				}
		
			//method calls and others
			case ArrayCell(tab, idx) => tcExpr(tab, TArray); tcExpr(idx, TInt)
			case Length(tab) => tcExpr(tab, TArray); TInt
			case mc @ MethodCall(_, _, _) => typeCheckMethodCall(mc)

			//"base" expressions
			case IntLit(v) => TInt
			case StringLit(l) => TString
			case BoolLit(b) => TBool
			case ArrayCreation(s) => tcExpr(s, TInt); TArray
			case Instanciation(id) => TObject(id.getSymbol.asInstanceOf[ClassSymbol])

			case id @ Identifier(v) => id.getSymbol.asInstanceOf[VariableSymbol].getType

			// shouldn't happen, but makes the compiler happy!
			case _ => throw new Exception("Shouldn't happen")
		}

		expr.setType(t)

		exp.foreach(g => if(t.isSubTypeOf(g)) return t)
		if(exp.length != 0){
			var err: String = "Expected "
			exp.foreach(g => err = err + g + " ")
			err = err + ", found " + t
			error(err, expr)
			exp(0)
		}else
			t
	}

	/**
	 * This method type checks method calls and exception handling, we cannot do it sooner since we do not know
	 * the method symbol before
	 */
	private def typeCheckMethodCall(mc: MethodCall): Type = {
		val cl: ClassSymbol = tcExpr(mc.instanceId) match{
			case TObject(cla) => cla
			case _ => error("Cannot call method on non-object type", mc.methodId.pos); return TError
		}
		val mt: MethodSymbol = cl.lookupMethod(mc.methodId.value) match{
			case Some(p) => p
			case None => error("No such method : " + mc.methodId.value + " in class " + cl.name, mc.pos); return TError
		}
		if(mc.args.length != mt.argList.length){
			error("Wrong number of arguments", mc.pos)
		}else{
			typeCheckMethodCallParams(mc.args, mt.argList)
		}
		mc.methodId.setSymbol(mt)
		mt.getType
	}

	private def typeCheckMethodCallParams(l1: List[ExprTree], l2: List[VariableSymbol]): Unit = l1 match{
		case x :: xs => 
			if(!tcExpr(x).isSubTypeOf(l2.head.getType)){
				error("Expected " + l2.head.getType + " found " + tcExpr(x), x.pos)
			}
			typeCheckMethodCallParams(xs, l2.tail)
		case Nil =>
	}

    /** for statements... */
	def tcStat(stat: StatTree): Unit = stat match{
		case Println(expr) => tcExpr(expr, TInt, TBool, TString)
		case Assign(id, expr) => 
			if(!tcExpr(expr).isSubTypeOf(id.getSymbol.asInstanceOf[VariableSymbol].getType)){
				error("Two sides of an assignment must have the same type", stat.pos)
			}
		case AssignArrayCell(id, idx, expr) =>
			tcExpr(idx, TInt)
			tcExpr(id, TArray)
			tcExpr(expr, TInt)
		case IfElse(test, stat1, stat2) => tcExpr(test, TBool); tcStat(stat1); tcStat(stat2)
		case If(test, stat) => tcExpr(test, TBool); tcStat(stat)
		case While(test, stat) => tcExpr(test, TBool); tcStat(stat)
		case Block(stats) => stats.foreach(g => tcStat(g))

        case TryCatchBlock(body, cs, Some(f)) => tcStat(TryCatchBlock(body, cs, None)); tcStat(f)
		case TryCatchBlock(body, cs, None) =>
			tcStat(body)
			cs.foreach(g => tcStat(g))

		case Catch(excId, excTy, body) => 
			tcStat(body)
			if(!isThrowable(excId.getSymbol.asInstanceOf[ExceptionSymbol].getType))
				error(excTy.value + " isn't an exception", stat.pos)
        case Finally(body) => 
			tcStat(body)
		case Throw(expr) =>
			if(!isThrowable(tcExpr(expr)))
				error("Invalid throw statement", stat.pos)

		case _ =>
	}

	def isThrowable(t: Type): Boolean = t match{
		case typ @ TObject(_) =>
			typ isSubTypeOf TObject(InitialSymbols.toolException)
		case _ => false
	}
}
